package com.gwt2go.dev.linker;

import java.io.IOException;
import java.util.Arrays;
import java.util.Date;
import java.util.Iterator;
import java.util.List;

import org.apache.commons.io.IOUtils;

import com.google.gwt.core.ext.LinkerContext;
import com.google.gwt.core.ext.TreeLogger;
import com.google.gwt.core.ext.UnableToCompleteException;
import com.google.gwt.core.ext.linker.AbstractLinker;
import com.google.gwt.core.ext.linker.Artifact;
import com.google.gwt.core.ext.linker.ArtifactSet;
import com.google.gwt.core.ext.linker.ConfigurationProperty;
import com.google.gwt.core.ext.linker.EmittedArtifact;
import com.google.gwt.core.ext.linker.LinkerOrder;
import com.google.gwt.core.ext.linker.impl.SelectionInformation;

/**
 * Manifest linker class creating application manifest for HTML5 offline web
 * applications supporting browsers, based on code from
 * com.google.gwt.core.linker.SimpleAppCacheLinker <br>
 * 
 * @see <a
 *      href="http://code.google.com/p/google-web-toolkit/source/browse/trunk/dev/core/src/com/google/gwt/core/linker/SimpleAppCacheLinker.java?r=10181">SimpleAppCacheLinker</a>
 * 
 * <br>
 *      ManifestLinker - linker for public path resources in the Application
 *      Cache.
 *      <p>
 *      To use:
 *      <ol>
 *      <li>Add {@code manifest="YOURMODULENAME/appcache.manifest"} to the
 *      {@code <html>} tag in your base html file. E.g.,
 *      {@code <html manifest="mymodule/appcache.manifest">}</li>
 *      <li>Add a mime-mapping to your web.xml file:
 *      <p>
 * 
 *      <pre>
 * {@code <mime-mapping>
 * <extension>manifest</extension>
 * <mime-type>text/cache-manifest</mime-type>
 * </mime-mapping>
 * }
 * </pre>
 * 
 *      </li>
 *      </ol>
 *      <p>
 *      On every compile, this linker will regenerate the appcache.manifest file
 *      with files from the public path of your module.
 *      <p>
 *      To obtain a manifest that contains other files in addition to those
 *      generated by this linker, create a class that inherits from this one and
 *      overrides {@code otherCachedFiles()}, and use it as a linker instead:
 *      <p>
 * 
 *      <pre>
 * <blockquote>
 * {@code @Shardable}
 * public class MyAppCacheLinker extends ManifestLinker {
 *   {@code @Override}
 *   protected String[] otherCachedFiles() {
 *     return new String[] {"/MyApp.html","/MyApp.css"};
 *   }
 * 
 *   {@code @Override}
 *   protected String[] appCacheManifestTemplate() {
 *     return "myappcache.manifest";
 *   }
 * }
 * </blockquote>
 * </pre>
 * 
 * @author L.Pelov
 */
@LinkerOrder(LinkerOrder.Order.POST)
public class ManifestLinker extends AbstractLinker {

	private static final String MANIFEST = "appcache.manifest";
	private static final String MANIFESTTEMPLATE = "cache.manifest.template";
	private static final String CONFIG_PROPERTY = "cache.manifest";

	@Override
	public String getDescription() {
		return "Application manifest linker";
	}

	@Override
	public ArtifactSet link(TreeLogger logger, LinkerContext context,
			ArtifactSet artifacts, boolean onePermutation)
			throws UnableToCompleteException {
		ArtifactSet toReturn = new ArtifactSet(artifacts);

		if (onePermutation) {
			return toReturn;
		}

		if (toReturn.find(SelectionInformation.class).isEmpty()) {
			logger.log(TreeLogger.INFO, "Warning: Clobbering " + MANIFEST
					+ " to allow debugging. "
					+ "Recompile before deploying your app!");
			artifacts = null;
		} else {
			// Create the general cache-manifest resource for the landing page:
			toReturn.add(emitLandingPageCacheManifest(context, logger,
					artifacts));
		}
		return toReturn;

	};

	/**
	 * Creates the cache-manifest resource specific for the landing page.
	 * 
	 * @param context
	 *            the linker environment
	 * @param logger
	 *            the tree logger to record to
	 * @param artifacts
	 *            {@code null} to generate an empty cache manifest
	 */

	private Artifact<?> emitLandingPageCacheManifest(LinkerContext context,
			TreeLogger logger, ArtifactSet artifacts)
			throws UnableToCompleteException {
		StringBuilder publicSourcesSb = new StringBuilder();
		StringBuilder staticResoucesSb = new StringBuilder();

		if (artifacts != null) {
			// Iterate over all emitted artifacts, and collect all cacheable
			// artifacts
			for (@SuppressWarnings("rawtypes")
			Artifact artifact : artifacts) {
				if (artifact instanceof EmittedArtifact) {
					EmittedArtifact ea = (EmittedArtifact) artifact;
					String pathName = ea.getPartialPath();
					if (pathName.endsWith("symbolMap")
							|| pathName.endsWith(".xml.gz")
							|| pathName.endsWith("rpc.log")
							|| pathName.endsWith("gwt.rpc")
							|| pathName.endsWith("manifest.txt")
							|| pathName.startsWith("rpcPolicyManifest")) {
						// skip these resources
					} else {
						publicSourcesSb.append(pathName + "\n");
					}
				}
			}

			String[] cacheExtraFiles = getPropsExtraFiles(logger, context);

			if (cacheExtraFiles.length == 0) {
				cacheExtraFiles = getCacheExtraFiles();
			}

			for (int i = 0; i < cacheExtraFiles.length; i++) {
				staticResoucesSb.append(cacheExtraFiles[i]);
				staticResoucesSb.append("\n");
			}

		}

		String cacheManifestString = createCache(logger, context,
				publicSourcesSb, staticResoucesSb);

		// Create the manifest as a new artifact and return it:
		return emitString(logger, cacheManifestString, MANIFEST);
	}

	/**
	 * Generate the application cache manifest file
	 * 
	 * @param logger
	 *            - the tree logger to record to
	 * @param context
	 *            - the linker environment
	 * @param publicSourcesSb
	 *            - contains the public resources, generated by the compilation
	 * @param staticResoucesSb
	 *            - contains the static resources, defined by your subclass
	 * @return
	 * @throws UnableToCompleteException
	 */
	protected String createCache(TreeLogger logger, LinkerContext context,
			StringBuilder publicSourcesSb, StringBuilder staticResoucesSb)
			throws UnableToCompleteException {

		// // build cache list
		// StringBuilder sb = new StringBuilder();
		// sb.append("CACHE MANIFEST\n");
		// sb.append("# Unique id #" + (new Date()).getTime() + "."
		// + Math.random() + "\n");
		// // we have to generate this unique id because the resources can
		// change
		// // but the hashed cache.html files can remain the same.
		// sb.append("# Note: must change this every time for cache to invalidate\n");
		// sb.append("\n");
		// sb.append("CACHE:\n");
		// sb.append("# Static app files\n");
		// sb.append(staticResoucesSb.toString());
		// sb.append("\n# Generated app files\n");
		// sb.append(publicSourcesSb.toString());
		// sb.append("\n\n");
		// sb.append("# All other resources require the user to be online.\n");
		// sb.append("NETWORK:\n");
		// sb.append("*\n");

		try {

			String manifest = IOUtils.toString(getClass().getResourceAsStream(
					appCacheManifestTemplate()));

			// replace the placeholder with the real data
			manifest = manifest.replace("$UNIQUEID$",
					(new Date()).getTime() + "." + Math.random()).toString();

			manifest = manifest.replace("$STATICAPPFILES$",
					staticResoucesSb.toString());

			manifest = manifest.replace("$GENAPPFILES$",
					publicSourcesSb.toString());

			logger.log(
					TreeLogger.INFO,
					"Be sure your landing page's <html> tag declares a manifest:"
							+ " <html manifest="
							+ context.getModuleFunctionName() + "/" + MANIFEST
							+ "\">");

			return manifest;

		} catch (IOException e) {

			logger.log(TreeLogger.ERROR,
					"Could not read cache manifest template.", e);

			throw new UnableToCompleteException();
		}
	}

	private String[] getCacheExtraFiles() {
		String[] cacheExtraFiles = otherCachedFiles();
		return cacheExtraFiles == null ? new String[0] : Arrays.copyOf(
				cacheExtraFiles, cacheExtraFiles.length);
	}

	/**
	 * Obtains the extra files to include in the manifest. Ensures the returned
	 * array is not null.
	 */
	protected String[] otherCachedFiles() {
		return null;
	}

	/**
	 * Do not forget to include this file into your <html manifest="" \>
	 * 
	 * @return
	 */
	protected String appCacheManifestTemplate() {
		return MANIFESTTEMPLATE;
	}

	protected String[] getPropsExtraFiles(TreeLogger logger,
			LinkerContext context) {

		// get the configured external properties, if any
		// try to list the configuration properties here
		// this files should be static external files

		if (context.getConfigurationProperties().isEmpty()) {
			logger.log(
					TreeLogger.INFO,
					"Info: No external static options "
							+ " have been configured, configuration-property cache.manifest "
							+ " is empty!");

			return new String[0];

		} else {

			Iterator<ConfigurationProperty> it = context
					.getConfigurationProperties().iterator();

			while (it.hasNext()) {

				ConfigurationProperty prop = it.next();

				if (prop.getName().equalsIgnoreCase(CONFIG_PROPERTY)) {

					List<String> props = prop.getValues();

					return (String[]) props.toArray(new String[0]);
				}

			}

		}

		return new String[0];

	}
}
